<script lang="ts" module>
    export const AnchorContextKey = Symbol("AnchorContextKey");
    export const useAnchorContext = () => getContext<AnchorContextData>(AnchorContextKey);
</script>

<script lang="ts">
    import { getContext, onDestroy, onMount, setContext, untrack } from "svelte";

    import { useClassList } from "../utils/useClassList";
    import type { AnchorContextData, AnchorLinkProps, AnchorProps, AnchorStore } from "./Anchor";
    import { scrollTop } from "../utils/utils";

    const props: AnchorProps = $props();
    const { bounds = 5, mode = "hash", showInk = false, onchange, scrollOffset, offsetTop, container, ...rest } = props;
    let scrollContainer: any = null;
    let scrollElement: any = null;
    let wrapperTop = 0;
    let titlesOffsetArr: any = [];
    let linkMap: any = {};

    const classList = $derived(
        useClassList(props, "cm-anchor", {
            [`cm-anchor-${props.size}`]: props.size,
        })
    );

    const store = $state({
        inkTop: 0,
        inkHeight: 0,
        currentId: "",
        currentLink: "",
        animating: false,
        links: [],
        flatLinks: [],
        upperFirstTitle: true,
    } as AnchorStore);

    $effect(() => {
        props.onchange?.(store.currentId);
    });

    const handleHashChange = () => {
        let sharpLinkMatch: any;
        if (mode === "hash") {
            const url = window.location.href;
            sharpLinkMatch = /#([^#]+)$/.exec(url);
        } else {
            const path = window.location.href;
            const search = path.includes("?") ? path.split("?")[1] : "";
            const params = new URLSearchParams(search);
            const has = params.has("_to");
            if (has) {
                if (params.get("_to")) {
                    sharpLinkMatch = [];
                    sharpLinkMatch[0] = params.get("_to");
                    sharpLinkMatch[1] = params.get("_to")?.replace("#", "");
                }
            }
        }

        if (!sharpLinkMatch) {
            setTimeout(() => {
                const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
                getCurrentScrollAtTitleId(scrollTop);
            }, 10);
            return;
        }

        store.currentLink = sharpLinkMatch[0];
        store.currentId = sharpLinkMatch[1];
    };

    const removeListener = () => {
        scrollContainer && scrollContainer.removeEventListener("scroll", handleScroll);
        window.removeEventListener("hashchange", handleHashChange);
    };

    const handleScroll = (e: any) => {
        if (store.animating) return;
        const scrollTop = document.documentElement.scrollTop || document.body.scrollTop || e.target.scrollTop;
        getCurrentScrollAtTitleId(scrollTop);
    };

    const handleScrollTo = () => {
        const anchor = document.getElementById(store.currentId);
        const currentLinkElementA: any = document.querySelector(`a[data-href="${store.currentLink}"]`);
        let offset = props.scrollOffset || 0;
        if (currentLinkElementA) {
            offset = parseFloat(currentLinkElementA.getAttribute("data-scroll-offset"));
        }
        if (!anchor) return;
        const offsetTop = anchor.offsetTop - wrapperTop - offset;

        store.animating = true;
        scrollTop(scrollContainer, scrollElement.scrollTop, offsetTop, 600, () => {
            store.animating = false;
        });
    };

    $effect(() => {
        store.currentLink;
        const currentLinkElementA: any = document.querySelector(`a[data-href="${store.currentLink}"]`);

        if (!currentLinkElementA) return;
        const elementATop = currentLinkElementA.offsetTop;
        const elementAHeight = currentLinkElementA.getBoundingClientRect().height;
        const offset = elementAHeight / 4;

        const top = elementATop < 0 ? props.offsetTop || 0 : elementATop;
        untrack(() => {
            store.inkTop = top + offset / 2;
            store.inkHeight = (elementAHeight * 3) / 4;
        });
    });

    const getContainer = () => {
        scrollContainer = props.container ? (typeof props.container === "string" ? document.querySelector(props.container) : props.container) : window;
        scrollElement = props.container ? scrollContainer : document.documentElement || document.body;
    };

    const getCurrentScrollAtTitleId = (scrollTop: number) => {
        let i = -1;
        const len = titlesOffsetArr.length;
        let titleItem = {
            link: "#",
            offset: 0,
        };

        scrollTop += bounds;
        while (++i < len) {
            const currentEle = titlesOffsetArr[i];
            const nextEle = titlesOffsetArr[i + 1];
            if (scrollTop >= currentEle?.offset && scrollTop < ((nextEle && nextEle.offset) || Infinity)) {
                titleItem = titlesOffsetArr[i];
                break;
            }
        }
        store.currentLink = titleItem.link;
    };

    const containerIsWindow = $derived(scrollContainer === window);

    const init = () => {
        handleHashChange();
        setTimeout(() => {
            removeListener();
            getContainer();
            wrapperTop = containerIsWindow ? 0 : scrollElement.offsetTop;
            handleScrollTo();
            scrollContainer.addEventListener("scroll", handleScroll);
            window.addEventListener("hashchange", handleHashChange);
        }, 0);
    };

    $effect(() => {
        const links = store.links.map((item) => {
            return item.href;
        });
        untrack(() => {
            const idArr = links.map((link) => {
                return link.split("#")[1];
            });

            if (!scrollElement) {
                getContainer();
            }

            const arr: any = [];
            idArr.forEach((id) => {
                const titleEle = document.getElementById(id);
                if (titleEle)
                    arr.push({
                        link: `#${id}`,
                        offset: titleEle.offsetTop - scrollElement.offsetTop,
                    });
            });

            titlesOffsetArr = arr;
        });
    });

    const gotoAnchor = (href: any, e: any) => {
        e.stopPropagation && e.stopPropagation();
        e.preventDefault && e.preventDefault();

        store.currentLink = href;
        store.currentId = href.replace("#", "");
        handleScrollTo();
        if (mode === "hash") {
            window.location.hash = href;
        } else {
            const path = window.location.href;
            const search = path.includes("?") ? path.split("?")[1] : "";
            const index = location.hash.indexOf("?");
            const hash = index > -1 ? location.hash.substring(0, index) : location.hash;
            const params = new URLSearchParams(search);
            params.set("_to", href);
            window.history.replaceState({}, "", `${location.pathname}${hash}?${params.toString()}`);
        }
    };

    onMount(() => {
        init();
        const timer = setInterval(() => {
            const links = store.flatLinks.map((item) => {
                return item.href;
            });
            const idArr = links.map((link) => {
                return link.split("#")[1];
            });
            idArr.forEach((id, index) => {
                const titleEle = document.getElementById(id);
                if (titleEle) {
                    const offset = titleEle.offsetTop - scrollElement.offsetTop;
                    if (!titlesOffsetArr[index]) {
                        titlesOffsetArr[index] = {
                            link: `#${id}`,
                            offset: 0,
                        };
                    }
                    if (titlesOffsetArr[index] && titlesOffsetArr[index]?.offset !== offset) {
                        titlesOffsetArr[index].offset = offset;
                        titlesOffsetArr[index].link = `#${id}`;
                    }
                }
            });
        }, 500);

        onDestroy(() => {
            clearInterval(timer);
        });
    });

    onDestroy(() => {
        removeListener();
    });

    setContext(AnchorContextKey, {
        level: 1,
        id: "",
        scrollOffset: props.scrollOffset,
        addLink: (parentId: string, link: any) => {
            link.parentId = parentId;
            linkMap[link.id] = link;
            if (!parentId) {
                store.links.push(link);
            } else {
                const p = linkMap[parentId];
                if (p) {
                    p.subItems = p.subItems || [];
                    p.subItems.push(link);
                }
            }
            store.flatLinks.push(link);
        },
    } as AnchorContextData);
</script>

{#snippet renderLinks(arr: AnchorLinkProps[])}
    {#if arr?.length}
        {#each arr as link, index (index)}
            <div class="cm-anchor-link">
                <a
                    class="cm-anchor-link-title"
                    href={link.href}
                    data-scroll-offset={props.scrollOffset || 0}
                    data-href={link.href}
                    onclick={(e: any) => {
                        gotoAnchor(link.href, e);
                    }}
                    title={link.title as string}>{link.title}</a
                >
                {#if link.subItems?.length}
                    {@render renderLinks(link.subItems)}
                {/if}
            </div>
        {/each}
    {/if}
{/snippet}

{@render props.children?.()}
<div {...rest} class={classList}>
    <div class="cm-anchor-wrapper">
        <div class="cm-anchor-inner">
            <div class={"cm-anchor-ink " + (showInk ? "cm-anchor-show" : "")}>
                <span class="cm-anchor-ink-ball" style={`top: ${store.inkTop}px; height: ${store.inkHeight}px `}></span>
            </div>
            {@render renderLinks(store.links)}
        </div>
    </div>
</div>
